unit gameproject;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Graphics,

  mapinfolist, database, constants, fpjson, tilesetgraphicsunit, BGRABitmap,
  BGRABitmapTypes;

type

  { TGameProject }

  TBattleStyle = (bsAutoDetect, bsSideView, bsFrontView);
  TBackgroundType = (btBattle, btTitle);

  TGameProject = class
  public
    MapInfos: TMapInfoList;
    Db: TDatabase;
    DbIsDirty, MapIsDirty: Boolean;
    MapId: Integer;
    Map: TJSONObject;
    TilesetGraphics: TTilesetGraphics;
    IconGraphics: TPortableNetworkGraphic;
    Dir: String;

    constructor Create;
    procedure Open(Filename: String);
    procedure Save;
    destructor Destroy; override;
    function GetImageFilename(Image, Subfolder: String): String;
    function GetImageSubfolderPath(Subfolder: String): String;
    function GetFace(ImageName: String; Index: Integer; ResultWidth: Integer = FACE_WIDTH): TBitmap;
    function GetIcon(Index: Integer): TBitmap;
    function GetCharacterGraphics(ImageName: String; Index: Integer;
                                  Direction: Integer = 2; Pattern: Integer = 1): TBitmap;
    function GetSvBattlerGraphics(ImageName: String; Motion: Integer = 0; Pattern: Integer = 0): TBitmap;
    function GetEnemyGraphics(ImageName: String; HueShift: Integer;
                                  MaxWidth: Integer = -1;
                                  MaxHeight: Integer = -1;
                                  BattleStyle: TBattleStyle = bsAutoDetect): TBitmap;
    function GetEnemyGraphicsBGRA(ImageName: String; HueShift: Integer;
                                  Scale: Double = 1/3;
                                  BattleStyle: TBattleStyle = bsAutoDetect): TBGRABitmap;
    function GetScaledBattlebackImgBGRA(ShortFn: String; BattlebackNumber: Integer; Scale: Double): TBGRACustomBitmap;
    function GetScaledTitleImgBGRA(ShortFn: String; TitleNumber: Integer; Scale: Double): TBGRACustomBitmap;
    function GetScaledImgBGRA(ShortFn: String; Subfolder: String; Scale: Double): TBGRACustomBitmap;
    function GetBackgroundBGRA(BgType: TBackgroundType; Bg1, Bg2: String; Scale: Double = 1/3): TBGRABitmap;
    function FormatPositionName(AMapId, X, Y: Integer): String;


    class function IsDollarName(S: String): Boolean; inline;
    function LoadMap(AMapId: Integer): TJSONObject;
    procedure OpenMap(AMapId: Integer);
    procedure SaveMap(AMap: TJSONObject; AMapId: Integer);
    function GetMapFilename(AMapId: Integer): String;
  end;

implementation

uses
  strutils, jsonparser, imghelper;

constructor TGameProject.Create;
begin
  TilesetGraphics := TTilesetGraphics.Create;
  Randomize;
end;

procedure TGameProject.Open(Filename: String);
begin
  Dir := ExtractFileDir(Filename);
  MapInfos := TMapInfoList.Create();
  MapInfos.Open(Dir + DirectorySeparator + 'data'
                    + DirectorySeparator + 'MapInfos.json');

  Db := TDatabase.Create;
  Db.Open(Dir + DirectorySeparator + 'data');

  IconGraphics := TPortableNetworkGraphic.Create;
  try
    IconGraphics.LoadFromFile(GetImageFilename('IconSet', 'system'));
  except
    IconGraphics.Free;
    IconGraphics := nil;
  end;

  DbIsDirty := False;
end;

procedure TGameProject.Save;
begin
  //TODO: save map
  if DbIsDirty then
    Db.Save;
  DbIsDirty := False;

  if MapIsDirty then
    SaveMap(Map, MapId);
  MapIsDirty := False;
end;

destructor TGameProject.Destroy;
begin
  if MapInfos <> nil then
    MapInfos.Free;
  if Db <> nil then
    Db.Free;
  if TilesetGraphics <> nil then
    TilesetGraphics.Free;
  if IconGraphics <> nil then
    IconGraphics.Free;

  inherited;
end;

function TGameProject.GetImageFilename(Image, Subfolder: String): String;
begin
  GetImageFilename := Dir + DirectorySeparator + 'img'
                          + DirectorySeparator + Subfolder
                          + DirectorySeparator + Image + '.png';
end;

function TGameProject.GetImageSubfolderPath(Subfolder: String): String;
begin
  GetImageSubfolderPath := Dir + DirectorySeparator + 'img'
                               + DirectorySeparator + Subfolder;
end;

function TGameProject.GetFace(ImageName: String; Index: Integer;
                              ResultWidth: Integer = FACE_WIDTH): TBitmap;
var
  FacePng: TPortableNetworkGraphic;
  CroppedFace, ResizedFace: TBitmap;
  Src, Dst: TRect;
begin
  ResizedFace := TBitmap.Create;
  ResizedFace.SetSize(ResultWidth, FACE_HEIGHT * ResultWidth div FACE_WIDTH);

  if ImageName <> '' then begin
    FacePng := TPortableNetworkGraphic.Create;
    try
      FacePng.LoadFromFile(GetImageFilename(ImageName, 'faces'));

      CroppedFace := TBitmap.Create;
      try
        CroppedFace.SetSize(FACE_WIDTH, FACE_HEIGHT);

        ResizedFace := TBitmap.Create;
        ResizedFace.SetSize(ResultWidth, FACE_HEIGHT * ResultWidth div FACE_WIDTH);

        CroppedFace.Canvas.Brush.Color := clBtnFace;
        CroppedFace.Canvas.FillRect(0,0, FACE_WIDTH, FACE_HEIGHT);

        Src.Top := (Index div 4) * FACE_HEIGHT;
        Src.Left := (Index mod 4) * FACE_WIDTH;
        Src.Right := Src.Left + FACE_WIDTH;
        Src.Bottom := Src.Top + FACE_HEIGHT;

        Dst := Rect(0, 0, FACE_WIDTH, FACE_HEIGHT);

        CroppedFace.Canvas.CopyRect(Dst,
                                    FacePng.Canvas,
                                    Src);


        Dst := Rect(0, 0, ResizedFace.Width, ResizedFace.Height);
        ResizedFace.Canvas.StretchDraw(Dst, CroppedFace);
      finally
        CroppedFace.Free;
      end;
    finally
      FacePng.Free;
    end;

    GetFace := ResizedFace;
  end
  else begin
    ResizedFace.Canvas.Brush.Color := clWhite;
    ResizedFace.Canvas.FillRect(0,0, FACE_WIDTH, FACE_HEIGHT); //TODO draw checkboard
  end;

  GetFace := ResizedFace;
end;

function TGameProject.GetIcon(Index: Integer): TBitmap;
var
  CroppedIcon: TBitmap;
  Src, Dst: TRect;
begin
  CroppedIcon := TBitmap.Create;
  CroppedIcon.SetSize(32, 32); //TODO: allow different icon sizes

  CroppedIcon.Canvas.Brush.Color := clBtnFace;
  CroppedIcon.Canvas.FillRect(0, 0, 32, 32);

  Src.Top := (Index div 16) * 32;
  Src.Left := (Index mod 16) * 32;
  Src.Right := Src.Left + 32;
  Src.Bottom := Src.Top + 32;

  Dst := Rect(0, 0, 32, 32);

  CroppedIcon.Canvas.CopyRect(Dst,
                              IconGraphics.Canvas,
                              Src);

  GetIcon := CroppedIcon;
end;

function TGameProject.GetMapFilename(AMapId: Integer): String;
var
  Num, Fn: String;
begin
  Num := IntToStr(AMapId);
  Fn := 'Map' + AddChar('0', Num, 3);
  GetMapFilename := Dir + DirectorySeparator + 'data'
                          + DirectorySeparator + Fn + '.json';
end;

procedure TGameProject.OpenMap(AMapId: Integer);
var
  MapFilename: String;
  Stream: TFileStream;
  Parser: TJSONParser;
begin
  if Map <> nil then
    Map.Free;

  MapId := AMapId;
  Map := LoadMap(MapId);

  TilesetGraphics.SetTileset(Db.Tilesets.Objects[Map.Integers['tilesetId']]);
end;

procedure TGameProject.SaveMap(AMap: TJSONObject; AMapId: Integer);
var
  Stream: TFileStream;
  StringData: UTF8String;
  FullFilename: String;
begin
  if AMap = nil then
    Exit;

  FullFilename := GetMapFilename(AMapId);
  Stream := TFileStream.Create(FullFilename, fmCreate or fmShareExclusive);
  try
    StringData := AMap.AsJSON;
    Stream.Write(StringData[1], Length(StringData));
  finally
    Stream.Free;
  end;
end;

function TGameProject.GetCharacterGraphics(ImageName: String; Index: Integer;
                              Direction: Integer = 2; Pattern: Integer = 1): TBitmap;
var
  Png: TPortableNetworkGraphic;
  CroppedGfx: TBitmap;
  Src, Dst: TRect;
  Row, Column, CellWidth, CellHeight: Integer;
begin
  Row := ((Direction div 2) - 1) mod 4;
  Column := Pattern mod 3;

  if not IsDollarName(ImageName) then begin
    Row := Row + (Index div 4) * 4;
    Column := Column + (Index mod 4) * 3;
  end;

  Png := TPortableNetworkGraphic.Create;
  try
    Png.LoadFromFile(GetImageFilename(ImageName, 'characters'));

    if IsDollarName(ImageName) then begin
      CellWidth := Png.Width div 3;
      CellHeight := Png.Height div 4;
    end
    else begin
      CellWidth := Png.Width div (3 * 4);
      CellHeight := Png.Height div (2 * 4);
    end;

    CroppedGfx := TBitmap.Create;
    CroppedGfx.SetSize(CellWidth, CellHeight);

    CroppedGfx.Canvas.Brush.Color := clBtnFace;
    CroppedGfx.Canvas.FillRect(0,0, CellWidth, CellHeight);

    Dst := Rect(0, 0, CellWidth, CellHeight);
    Src := Rect(Column * CellWidth,      Row * CellHeight,
                (Column+1) * CellWidth, (Row+1) * CellHeight);
    CroppedGfx.Canvas.CopyRect(Dst, Png.Canvas, Src);

    GetCharacterGraphics := CroppedGfx;
  finally
    Png.Free;
  end;
end;

function TGameProject.GetSvBattlerGraphics(ImageName: String;
                                Motion: Integer = 0; Pattern: Integer = 0): TBitmap;
var
  Png: TPortableNetworkGraphic;
  CroppedGfx: TBitmap;
  Src, Dst: TRect;
  Row, Column, CellWidth, CellHeight: Integer;
begin
  Row := Motion mod 6;
  Column := (Motion div 6) * 3 + Pattern;

  Png := TPortableNetworkGraphic.Create;
  try
    Png.LoadFromFile(GetImageFilename(ImageName, 'sv_actors'));

    CellWidth := Png.Width div 9;
    CellHeight := Png.Height div 6;

    CroppedGfx := TBitmap.Create;
    CroppedGfx.SetSize(CellWidth, CellHeight);

    CroppedGfx.Canvas.Brush.Color := clBtnFace;
    CroppedGfx.Canvas.FillRect(0,0, CellWidth, CellHeight);

    Dst := Rect(0, 0, CellWidth, CellHeight);
    Src := Rect(Column * CellWidth,      Row * CellHeight,
                (Column+1) * CellWidth, (Row+1) * CellHeight);
    CroppedGfx.Canvas.CopyRect(Dst, Png.Canvas, Src);

    GetSvBattlerGraphics := CroppedGfx;
  finally
    Png.Free;
  end;
end;

function TGameProject.GetEnemyGraphics(ImageName: String; HueShift: Integer;
  MaxWidth: Integer; MaxHeight: Integer; BattleStyle: TBattleStyle): TBitmap;
var
  Folder: String;
  Filename: String;
  Original: TBGRABitmap = nil;
  Recoloured: TBGRABitmap = nil;
  Resized: TBGRACustomBitmap;
  ImageToDraw: TBGRABitmap = nil;
  CroppedGfx: TBitmap = nil;
  Width, Height: Integer;
  Src, Dst: TRect;
begin
  GetEnemyGraphics := nil;

  if (BattleStyle = bsSideView) or ((BattleStyle = bsAutoDetect) and Db.IsSvBattle) then
    Folder := 'sv_enemies'
  else
    Folder := 'enemies';

  Filename := GetImageFilename(ImageName, Folder);
  if not FileExists(Filename) then
    Exit;

  try
    Original := TBGRABitmap.Create(Filename, True);
    if (HueShift <> 0) and (HueShift <> 360) then begin
      Recoloured := ChangeHue(Original, HueShift);
      ImageToDraw := Recoloured;
    end else
      ImageToDraw := Original;

    if MaxWidth = -1 then
      MaxWidth := ImageToDraw.Width;
    if MaxHeight = -1 then
      MaxHeight := ImageToDraw.Height;

    if (ImageToDraw.Height * MaxWidth div ImageToDraw.Width) > MaxHeight then begin
      Height := MaxHeight;
      Width := ImageToDraw.Width * MaxHeight div ImageToDraw.Height;
    end else begin
      Height := ImageToDraw.Height * MaxWidth div ImageToDraw.Width;
      Width := MaxWidth;
    end;

    CroppedGfx := TBitmap.Create;
    CroppedGfx.SetSize(Width, Height);

    CroppedGfx.Canvas.Brush.Color := clBtnFace;
    CroppedGfx.Canvas.FillRect(0, 0, Width, Height);

    try
      Resized := ImageToDraw.Resample(Width, Height);
      Resized.Draw(CroppedGfx.Canvas, 0, 0, False);
    finally
      Resized.Free;
    end;

    GetEnemyGraphics := CroppedGfx;
  finally
    Original.Free;
    if Recoloured <> nil then
      Recoloured.Free;
  end;
end;

function TGameProject.GetEnemyGraphicsBGRA(ImageName: String;
  HueShift: Integer; Scale: Double = 1/3;
  BattleStyle: TBattleStyle = bsAutoDetect): TBGRABitmap;
var
  Folder: String;
  Filename: String;
  Original: TBGRABitmap = nil;
  Resized: TBGRACustomBitmap;
  Recoloured: TBGRABitmap = nil;
  Width, Height: Integer;
  Src, Dst: TRect;
begin
  GetEnemyGraphicsBGRA := nil;

  if (BattleStyle = bsSideView) or ((BattleStyle = bsAutoDetect) and Db.IsSvBattle) then
    Folder := 'sv_enemies'
  else
    Folder := 'enemies';

  Filename := GetImageFilename(ImageName, Folder);
  if not FileExists(Filename) then
    Exit;

  try
    Original := TBGRABitmap.Create(Filename, True);

    Width := Round(Original.Width * Scale);
    Height := Round(Original.Height * Scale);

    try
      Resized := Original.Resample(Width, Height);
      if (HueShift <> 0) and (HueShift <> 360) then
         Recoloured := ChangeHue(Resized, HueShift)
      else
        Recoloured := TBGRABitmap.Create(Resized);

      GetEnemyGraphicsBGRA := Recoloured
    finally
      Resized.Free;
    end;
  finally
    Original.Free;
  end;
end;

function TGameProject.GetScaledBattlebackImgBGRA(ShortFn: String;
  BattlebackNumber: Integer; Scale: Double): TBGRACustomBitmap;
var
  FullFilename: String;
  Img: TBGRABitmap;
begin
  GetScaledBattlebackImgBGRA := GetScaledImgBGRA(
                                  ShortFn,
                                  'battlebacks' + IntToStr(BattlebackNumber),
                                  Scale);
end;

function TGameProject.GetScaledTitleImgBGRA(ShortFn: String;
  TitleNumber: Integer; Scale: Double): TBGRACustomBitmap;
begin
  GetScaledTitleImgBGRA := GetScaledImgBGRA(
                             ShortFn,
                             'titles' + IntToStr(TitleNumber),
                             Scale);
end;

function TGameProject.GetScaledImgBGRA(ShortFn: String; Subfolder: String;
  Scale: Double): TBGRACustomBitmap;
var
  FullFilename: String;
  Img: TBGRABitmap;
begin
  GetScaledImgBGRA := nil;

  FullFilename := GetImageFilename(ShortFn, Subfolder);
  if FileExists(FullFilename) then begin
    Img := TBGRABitmap.Create(FullFilename);
    try
      GetScaledImgBGRA := Img.Resample(Round(Img.Width * Scale),
                                       Round(Img.Height * Scale));
    finally
      Img.Free;
    end;
  end;
end;

function TGameProject.GetBackgroundBGRA(BgType: TBackgroundType; Bg1,
  Bg2: String; Scale: Double): TBGRABitmap;
var
  Bg1Scaled: TBGRACustomBitmap = nil;
  Bg2Scaled: TBGRACustomBitmap = nil;
  ResultImg: TBGRABitmap = nil;
begin
  GetBackgroundBGRA := nil;

  if BgType = btBattle then begin
    Bg1Scaled := GetScaledBattlebackImgBGRA(Bg1, 1, Scale);
    Bg2Scaled := GetScaledBattlebackImgBGRA(Bg2, 2, Scale);
  end else if BgType = btTitle then begin
    Bg1Scaled := GetScaledTitleImgBGRA(Bg1, 1, Scale);
    Bg2Scaled := GetScaledTitleImgBGRA(Bg2, 2, Scale);
  end else
    Exit;

  if Bg1Scaled <> nil then begin
    ResultImg := TBGRABitmap.Create(Bg1Scaled);
    if Bg2Scaled <> nil then
      ResultImg.CanvasBGRA.Draw(0, 0, Bg2Scaled);
  end else if Bg2Scaled <> nil then begin
    ResultImg := TBGRABitmap.Create(Bg2Scaled);
  end;

  GetBackgroundBGRA := ResultImg;
end;

function TGameProject.FormatPositionName(AMapId, X, Y: Integer): String;
var
  MapName: String;
begin
  if AMapId <> 0 then begin
    if MapInfos <> nil then
      MapName := MapInfos.GetMapName(AMapId)
    else
      MapName := Format('%0.3d',[AMapId]);

    FormatPositionName := rsMapCoordinates.Format([MapName, X, Y]);
  end else
    FormatPositionName := rsNoMapCoordinates;
end;

class function TGameProject.IsDollarName(S: String): Boolean; inline;
begin
  IsDollarName := False;

  if (Length(S) >= 1) and (S[1] = '$') then
    IsDollarName := True;

  if (Length(S) >= 2) and (S[1] = '!') and (S[2] = '$')  then
    IsDollarName := True;
end;

function TGameProject.LoadMap(AMapId: Integer): TJSONObject;
var
  MapFilename: String;
  Stream: TFileStream;
  Parser: TJSONParser;
begin
  MapFilename := GetMapFilename(AMapId);
  Stream := TFileStream.Create(MapFilename, fmOpenRead);
  try
    Parser := TJSONParser.Create(Stream);
    try
      LoadMap := Parser.Parse as TJSONObject;
    finally
      Parser.Free;
    end;
  finally
    Stream.Free;
  end;
end;

end.

